import os
import select
from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR

from beeos import Process
from log import Log

log = Log(tag='HTTP')


class TCPServer(Process):
    def __init__(self, host, port=80):
        self.host = host
        self.port = port
        self.socket = self._build_socket()

    def _build_socket(self):
        s = socket(AF_INET, SOCK_STREAM)
        s.setblocking(False)
        s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        s.bind((self.host, self.port))
        return s

    def setup(self):
        self.socket.listen(1)

    def loop(self):
        try:
            conn, addr = self.socket.accept()
        except OSError:
            pass
        else:
            self.on_accept(conn, addr)
        return True

    def on_accept(self, conn, addr):
        pass


ALL_EVENT = select.POLLIN | select.POLLOUT | select.POLLERR
POLL_TIMEOUT = 10


class PolledSocket:
    def __init__(self, skt, poller):
        self.socket = skt
        self.poller = poller

    def write(self, data):
        self.socket.write(data)

    def close(self):
        self.poller.unregister(self.socket)
        self.socket.close()

    def readline(self):
        return self.socket.readline()

    def read_all(self):
        return self.socket.read()

    def recv(self, size=1024):
        return self.socket.recv(size)


class AsyncTCPServer(TCPServer):
    def __init__(self, host, port=80):
        super().__init__(host, port)
        self.poller = select.poll()

    def on_accept(self, conn, addr):
        log.debug('Socket Connected [%s:%s]' % addr)
        self.poller.register(conn, ALL_EVENT)

    def loop(self):
        super().loop()
        events = self.poller.poll(POLL_TIMEOUT)
        if events:
            self._read_polled_events(events)
        return False

    def _read_polled_events(self, events):
        for sock, flag in events:
            if flag & select.POLLIN:
                p_sock = PolledSocket(sock, self.poller)
                try:
                    self.on_socket_ready(p_sock)
                except ServerError as e:
                    p_sock.write('Error : [%s]' % e)

    def on_socket_ready(self, sock):
        pass


class ServerError(Exception):
    def __init__(self, err):
        super().__init__(err)


class ServiceError(Exception):
    def __init__(self, err):
        super().__init__(err)


STATUS_TABLE = {
    200: 'OK',
    400: 'Bad_Request',
    401: 'Unauthorized',
    404: 'Not_Found',
    405: 'Method_Not_Allowed',
    500: 'Internal_Server_Error',
    502: 'Bad_Gateway',
}


class Response(object):
    def __init__(self, status=200, headers=None, body=b''):
        self.status = status
        self.headers = headers
        self.body = body

    @staticmethod
    def as_status_str(code):
        if code in STATUS_TABLE:
            return STATUS_TABLE[code]
        return str(code)

    def write_to_socket(self, sock):
        status = Response.as_status_str(self.status)
        status_line = 'HTTP/1.1 %s %s\r\n' % (self.status, status)
        sock.write(status_line)
        if self.headers:
            for name, value in self.headers:
                header_line = '%s: %s\r\n' % (name, value)
                sock.write(header_line)
        sock.write('Content-Length: %s\r\n' % len(self.body))
        sock.write('\r\n')
        sock.write(self.body)


class Request:
    def __init__(self, url, params, method, version, headers, body):
        self.url = url
        self.params = params
        self.method = method
        self.version = version
        self.headers = headers
        self.body = body


class ControllerMapper:
    def __init__(self):
        self.mapping = {}

    @staticmethod
    def is_file(name):
        try:
            return os.stat(name)
        except Exception as e:
            log.debug('File not found:[%s]' % name, e)
            return False

    def process_request(self, req):
        if req.url in self.mapping:
            c = self.mapping[req.url]
            m = getattr(c, req.method.lower(), None)
            if m:
                return m(req)
        if ControllerMapper.is_file(req.url):
            with open(req.url, 'r') as f:
                body = f.read()
            return Response(status=200, body=body.encode())
        return Response(status=404, body=b'<H1>404 Not Found!<H1>')

    def add_mapping(self, path, controller):
        self.mapping[path] = controller


class HttpRequestBody:
    def __init__(self, soc, headers):
        self.socket = soc
        self.headers = headers
        if 'Content-Length' not in self.headers:
            raise ServiceError('Can not obtain Content-Length from header!')
        self.length = headers['Content-Length']

    def read_all_as_string(self):
        length = int(self.length)
        log.info('Reading body with contentLength [%s]' % length)
        body = b''
        while len(body) < length:
            body += self.socket.recv(length - len(body))
        return body

    def get_length(self):
        return self.length


class HttpServer(AsyncTCPServer):
    def __init__(self, host='0.0.0.0', port=80):
        super().__init__(host, port)
        self.processor = ControllerMapper()

    @property
    def mapping(self):
        return self.processor

    def on_socket_ready(self, sock):
        try:
            httpline = sock.readline()
            path, params, method, version = HttpServer.unpack_http_line(httpline)
            if path == '/':
                path = '/index.html'
            log.info('resolve http request [%s]->[%s] with version:[%s]' % (method, path, version))
            headers = HttpServer.unpack_headers(sock)
            if method == 'GET':
                body = None
            else:
                body = HttpServer.unpack_body(sock, headers)
            req = Request(url=path, params=params, method=method, version=version, headers=headers, body=body)
            resp = self.processor.process_request(req)
        except ServerError as e:
            log.warn('Server Error ', e)
            resp = Response(status=500, body=b'<h1>Error [%s]</h1>' % e)
        except ServiceError as e:
            log.warn('Service Error ', e)
            resp = Response(status=400, body=b'<h1>Server Error [%s]</h1>' % e)
        except Exception as e:
            log.warn('Exception ', e)
            resp = Response(status=500, body=b'<h1>Other Error [%s]</h1>' % e)
        else:
            if not resp:
                resp = Response(body=b'<H1>Testing</H1>')
        try:
            resp.write_to_socket(sock)
        except Exception as e:
            log.warn('Error on Write Socket', e)
        try:
            sock.close()
        except Exception as e:
            log.warn('Error On CLose Socket', e)

    @staticmethod
    def unpack_headers(sock):
        headers = {}
        line = sock.readline()
        line = line.strip()
        line = line.decode()
        while line:
            name_and_value = line.split(':', 1)
            item_len = len(name_and_value)
            if item_len == 2:
                header_name = name_and_value[0]
                header_value = name_and_value[1]
            elif item_len == 1:
                header_name = name_and_value[0]
                header_value = None
            else:
                continue
            if header_value:
                header_value = header_value.strip()
            headers[header_name] = header_value
            line = sock.readline()
            line = line.strip()
            line = line.decode()
        return headers

    @staticmethod
    def unpack_http_line(line):
        line = line.strip().decode()
        groups = line.split(' ')
        if len(groups) != 3:
            log.warn('Invalidate Http Line [%s]' % line)
            return None
        method = groups[0]
        url = groups[1]
        version = groups[2]
        path, params = HttpServer.unpack_url(url)
        return path, params, method, version

    @staticmethod
    def unpack_url(url):
        url = url.strip()
        path_params = url.split('?')
        if not len(path_params) == 2:
            return url, {}
        path = path_params[0]
        p_str = path_params[1]
        parr = p_str.split('&')
        params = {}
        for s in parr:
            nvp = s.split('=')
            if not len(nvp) == 2:
                continue
            params[nvp[0]] = nvp[1]
        return path, params

    @staticmethod
    def unpack_body(sock, headers):
        return HttpRequestBody(sock, headers)
